OOPS Fundamentals


What is Inheritance?

Inheritance is used to indicate that one class will get most or all of its features from a parent class. This happens implicitly whenever you write class Foo(Bar), which says "Make a class Foo that inherits from Bar." When you do this, the language makes any action that you do on instances of Foo also work as if they were done to an instance of Bar. Doing this lets you put common functionality in the Bar class, then specialize that functionality in the Foo class as needed.

When you are doing this kind of specialization, there are three ways that the parent and child classes can interact:

  • Actions on the child imply an action on the parent.
  • Actions on the child override the action on the parent.
  • Actions on the child alter the action on the parent.

Also to note:

  • Implicit Inheritance: when you define a function in the parent, but not in the child.
  • Override Explicitly: when you define a function in the parent, and also in the child.

In [11]:
class Parent:
    def __init__(self):
        print("Parent init")
        
    def override(self):
        print( "PARENT override()")

    def implicit(self):
        print ("PARENT implicit()")

    def altered(self):
        print ("PARENT altered()")


class Child(Parent):
    def __init__(self):
        print("Child init")
        
    def override(self):
        print ("CHILD override()")

    def altered(self):
        print ("CHILD, BEFORE PARENT altered()")
        Parent.altered(self)  # Explicitly calling parent function
        print ("CHILD, AFTER PARENT altered()")

In [1]:
dad = Parent()
child = Child()

dad.implicit()
child.implicit()

dad.override()
child.override()

dad.altered()
child.altered()


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-1-5558a1b0fe23> in <module>()
----> 1 dad = Parent()
      2 child = Child()
      3 
      4 dad.implicit()
      5 child.implicit()

NameError: name 'Parent' is not defined

In [9]:
class Parent:
    def __init__(self):
        print("Parent init")
        
    def override(self, x=0):
        self.x = x
        print( "PARENT override()")

    def implicit(self):
        print ("PARENT implicit()")

    def altered(self):
        print ("PARENT altered()", self.x)


class Child(Parent):
    def __init__(self):
        print("Child init")

    def altered(self):
        print ("CHILD, BEFORE PARENT altered()")
        Parent.altered(self)  # Explicitly calling parent function
        print ("CHILD, AFTER PARENT altered()")

In [15]:
c, d = Child(), Child()

c.override(100)
d.override(20)
c.altered()
d.altered()


Child init
Child init
PARENT override()
PARENT override()
CHILD, BEFORE PARENT altered()
PARENT altered() 100
CHILD, AFTER PARENT altered()
CHILD, BEFORE PARENT altered()
PARENT altered() 20
CHILD, AFTER PARENT altered()

In [13]:
class Parent:
    x = 10
    def override(self):
        print( "PARENT override()")

    def implicit(self):
        print ("PARENT implicit()")

    def altered(self):
        print ("PARENT altered()")
    
    def update(self, val):
        self.x = val
    
class Child(Parent):

    def override(self):
        print ("CHILD override()")

    def altered(self):
        p = super(Child, self)
        print(type(p))
        print ("CHILD, BEFORE PARENT altered()")
        p.altered()
        print ("CHILD, AFTER PARENT altered()")

dad = Parent()
child1 = Child()
child2 = Child()

child1.update(100)
print(child1.x)
print(child2.x)


100
10

In [14]:
class Parent:
    x = 10
    
    def update(self, val):
        self.x = val
    
class Child(Parent):

    def altered(self, val):
        p = super(Child, self)
        p.update(val)

dad = Parent()
child1 = Child()
child2 = Child()

child1.altered(100)
print(child1.x)
print(child2.x)


100
10

The Reason for super()

This should seem like common sense, but then we get into trouble with a thing called multiple inheritance. Multiple inheritance is when you define a class that inherits from one or more classes, like this:

class SuperFun(Child, BadStuff):
    pass

This is like saying, "Make a class named SuperFun that inherits from the classes Child and BadStuff at the same time."

In this case, whenever you have implicit actions on any SuperFun instance, Python has to look-up the possible function in the class hierarchy for both Child and BadStuff, but it needs to do this in a consistent order. To do this Python uses "method resolution order" (MRO) and an algorithm called C3 to get it straight.

Because the MRO is complex and a well-defined algorithm is used, Python can't leave it to you to get the MRO right. Instead, Python gives you the super() function, which handles all of this for you in the places that you need the altering type of actions as I did in Child.altered. With super() you don't have to worry about getting this right, and Python will find the right function for you.

Using super() with init

The most common use of super() is actually in init functions in base classes. This is usually the only place where you need to do some things in a child, then complete the initialization in the parent. Here's a quick example of doing that in the Child:

class Child(Parent):

    def __init__(self, stuff):
        self.stuff = stuff
        super(Child, self).__init__()

This is pretty much the same as the Child.altered example above, except I'm setting some variables in the init before having the Parent initialize with its Parent.init.


In [ ]:


In [5]:
class Child(Parent):

    def __init__(self, stuff):
        self.stuff = stuff
        super(Child, self).__init__()

In [ ]:
help(super)